use std::fmt::Debug;

use aspect_codec::{inst_enum_codec, inst_wrapper_codec};
use dryoc::sign::SigningKeyPair;
use dryoc::{sign::IncrementalSigner, types::Bytes};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use aspect_codec::prelude::*;

/// Signature purpose tag, used to distinguish different use cases.
pub type SigPurpose = [u8; 4];

pub const IDSIG_PURPOSE_DUMMY: SigPurpose = [0xff; 4];
pub const IDSIG_PURPOSE_ANN: SigPurpose = [0, 0, 0, 1];
pub const IDSIG_PURPOSE_IDENT: SigPurpose = [0, 0, 0, 2];
pub const IDSIG_PURPOSE_PERSONAL: SigPurpose = [0, 0, 0, 3];
pub const IDSIG_PURPOSE_ASSOC_USER: SigPurpose = [0, 0, 0, 4];
pub const IDSIG_PURPOSE_ATTESTATION: SigPurpose = [0, 1, 0, 1];

#[derive(Debug, Error)]
pub enum Error {
    #[error("tried to encrypt to incompatible key type")]
    KeypairMismatch,

    #[error("dryoc: {0}")]
    CryptoDryoc(#[from] dryoc::Error),
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum KeyType {
    /// libsodium-style EC keys
    Ec,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum IdentKeypair {
    Ec(dryoc::sign::SecretKey),
}

impl IdentKeypair {
    pub fn to_pub(&self) -> IdentPubkey {
        match self {
            IdentKeypair::Ec(sk) => {
                let kp: SigningKeyPair<dryoc::sign::PublicKey, _> =
                    SigningKeyPair::from_secret_key(sk.clone());
                IdentPubkey::Ec(kp.public_key.clone())
            }
        }
    }

    pub fn ty(&self) -> KeyType {
        match self {
            Self::Ec(..) => KeyType::Ec,
        }
    }
}

inst_enum_codec! {
    IdentKeypair u8 {
        1 => Ec(frombuf dryoc::sign::SecretKey)
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum IdentPubkey {
    Ec(dryoc::sign::PublicKey),
}

impl IdentPubkey {
    pub fn ty(&self) -> KeyType {
        match self {
            Self::Ec(..) => KeyType::Ec,
        }
    }
}

inst_enum_codec! {
    IdentPubkey u8 {
        1 => Ec(frombuf dryoc::sign::PublicKey),
    }
}

#[derive(Clone, Serialize, Deserialize)]
pub struct Signature(#[serde(with = "serde_bytes")] Vec<u8>);

impl Signature {
    pub fn as_slice(&self) -> &[u8] {
        self.0.as_slice()
    }
}

impl Debug for Signature {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let l = self.as_slice().len();
        let show = &self.as_slice()[..usize::min(l, 4)];
        f.write_fmt(format_args!("sig({:?}...)", show))
    }
}

inst_wrapper_codec!(Signature => Vec<u8>);

/// Generates a fresh keypair of the given type.
pub fn gen_ident_keypair(ty: KeyType) -> IdentKeypair {
    match ty {
        KeyType::Ec => {
            use dryoc::sign::*;
            let kp: SigningKeyPair<PublicKey, SecretKey> = SigningKeyPair::gen();
            IdentKeypair::Ec(kp.secret_key.clone())
        }
    }
}

/// Signs a message using the given keypair.
pub fn sign_ident_msg(kp: &IdentKeypair, purpose: SigPurpose, msg: &[u8]) -> Signature {
    match kp {
        IdentKeypair::Ec(sk) => {
            let mut inc = dryoc::sign::IncrementalSigner::new();
            digest(&mut inc, purpose, msg);
            let sig: dryoc::sign::Signature = inc.finalize(sk).expect("sign: fuck");
            Signature(sig.to_vec())
        }
    }
}

/// Verifies a signature against a message and a pubkey.
pub fn verify_ident_msg(
    sig: &Signature,
    pk: &IdentPubkey,
    purpose: SigPurpose,
    msg: &[u8],
) -> bool {
    match pk {
        IdentPubkey::Ec(pk) => {
            let mut inc = dryoc::sign::IncrementalSigner::new();
            digest(&mut inc, purpose, msg);

            // Unpack the signature from the common representation.
            let sig_slice = sig.as_slice();
            let mut native_sig = dryoc::sign::Signature::new();
            if sig_slice.len() != native_sig.len() {
                return false;
            }
            native_sig.copy_from_slice(sig_slice);

            inc.verify(&native_sig, pk).is_ok()
        }
    }
}

/// Digest behavior split out in common so we can change it here and have it
/// work in both the sign and verify sides.
fn digest(signer: &mut IncrementalSigner, purpose: SigPurpose, msg: &[u8]) {
    signer.update(&purpose);
    signer.update(&msg);
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum InboxKeypair {
    Ec(dryoc::dryocbox::SecretKey),
}

impl InboxKeypair {
    pub fn to_pub(&self) -> InboxPubkey {
        match self {
            Self::Ec(sk) => {
                let kp = dryoc::dryocbox::KeyPair::from_secret_key(sk.clone());
                InboxPubkey::Ec(kp.public_key.clone())
            }
        }
    }

    pub fn ty(&self) -> KeyType {
        match self {
            Self::Ec(..) => KeyType::Ec,
        }
    }
}

inst_enum_codec! {
    InboxKeypair u8 {
        1 => Ec(frombuf dryoc::dryocbox::SecretKey),
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum InboxPubkey {
    Ec(dryoc::dryocbox::PublicKey),
}

inst_enum_codec! {
    InboxPubkey u8 {
        1 => Ec(frombuf dryoc::dryocbox::PublicKey),
    }
}

/// Generates a fresh inbox keypair of the given type.
pub fn gen_inbox_keypair(ty: KeyType) -> InboxKeypair {
    match ty {
        KeyType::Ec => {
            use dryoc::dryocbox::*;
            let kp = KeyPair::gen();
            InboxKeypair::Ec(kp.secret_key.clone())
        }
    }
}

/// Encrypts a message (without source authentication).
pub fn encrypt_inbox_msg(dest_pk: &InboxPubkey, msg: &[u8]) -> Result<Vec<u8>, Error> {
    match dest_pk {
        InboxPubkey::Ec(dpk) => {
            use dryoc::dryocbox::*;
            let buf = DryocBox::seal_to_vecbox(msg, dpk).expect("crypto: encrypt sealedbox");
            Ok(buf.to_vec())
        }

        #[allow(unused)]
        _ => Err(Error::KeypairMismatch),
    }
}

/// Decrypts a message (without source authentication).
pub fn decrypt_inbox_msg(local_kp: &InboxKeypair, msg: &[u8]) -> Result<Vec<u8>, Error> {
    match local_kp {
        InboxKeypair::Ec(lsk) => {
            use dryoc::dryocbox::*;
            // FIXME avoid extra allocs
            let dbox = DryocBox::<PublicKey, Mac, Vec<u8>>::from_sealed_bytes(msg)?;
            let lkp = KeyPair::from_secret_key(lsk.clone());
            let buf = dbox.unseal(&lkp)?;
            Ok(buf)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ident_sign() {
        let kp = gen_ident_keypair(KeyType::Ec);

        let msg = &[0xde, 0xad, 0xbe, 0xef];
        let sig = sign_ident_msg(&kp, IDSIG_PURPOSE_DUMMY, msg);

        assert!(verify_ident_msg(
            &sig,
            &kp.to_pub(),
            IDSIG_PURPOSE_DUMMY,
            msg
        ));
    }

    #[test]
    fn test_inbox_encdec() {
        let kp = gen_inbox_keypair(KeyType::Ec);

        let msg = &[0xca, 0xfe, 0xba, 0xbe];
        let cmsg = encrypt_inbox_msg(&kp.to_pub(), msg).expect("test: encrypt");
        let dmsg = decrypt_inbox_msg(&kp, &cmsg).expect("test: decrypt");

        assert_eq!(dmsg, msg);
    }
}
